#!/usr/bin/python
# Copyright (C) 2014 Christian Sengstock
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ____ ____ ____ _    ____ ____ ____ ___  
# |_ _\| __\| . \|\/\ |_ _\| . \|___\| _\ 
#   || |  ]_|  <_|   \  || |  <_| /  [__ \
#   |/ |___/|/\_/|/v\/  |/ |/\_/|/   |___/
#
# ASCII art credits to http://patorjk.com/software/taag
#
# Latest version: https://code.google.com/p/termtris/


"""
Termtris - A simple terminal Tetris game
"""

import sys
import curses
import random
import time

__author__ = "csengstock@gmail.com (Christian Sengstock)"
__version__ = 0.1



# relative stone coordinates
LINE = (((-1,0),(0,0),(1,0),(2,0)),
        ((0,-1),(0,0),(0,1),(0,2)),
        ((-1,0),(0,0),(1,0),(2,0)),
        ((0,-1),(0,0),(0,1),(0,2)))
QUAD = (((0,0),(0,1),(1,0),(1,1)),
        ((0,0),(0,1),(1,0),(1,1)),
        ((0,0),(0,1),(1,0),(1,1)),
        ((0,0),(0,1),(1,0),(1,1)))
LCOR = (((-1,0),(0,0),(1,0),(1,1)),
        ((1,-1),(0,-1),(0,0),(0,1)),
        ((-1,-1),(-1,0),(0,0),(1,0)),
        ((0,-1),(0,0),(0,1),(-1,1)))
RCOR = (((1,-1),(1,0),(0,0),(-1,0)),
        ((-1,-1),(0,-1),(0,0),(0,1)),
        ((-1,0),(-1,1),(0,0),(1,0)),
        ((0,-1),(0,0),(0,1),(1,1)))
TRI =  (((0,-1),(0,0),(0,1),(-1,0)),
        ((-1,0),(0,0),(1,0),(0,1)),
        ((0,-1),(0,0),(0,1),(1,0)),
        ((-1,0),(0,0),(1,0),(0,-1)))


class Stone:
    """
    Represents a single stone stepping downwards.
    Only one stone is stepping down at a time.
    After hitting the ground or an occupied cell,
    the stone coordinates get merged into the 'Field'.
    """
    TYPS = (LINE, QUAD, LCOR, RCOR, TRI)
    COLS = (2,3,4,5,6)

    def __init__(self, y, x, typ, field):
        # stone type
        self.typ = typ

        # stone state
        self.y = y
        self.x = x
        self.rot = 0

        # reference
        self.field = field

    def rotate(self,d):
        """
        Changes the rotation-state of the stone by
        d roations; +: forward, -: backward

        Checks if the rotation is possible
        for the referenced field and perfoms the rotation or
        skips the operation.
        """
        r = self.rot+d
        if r < 0:
            r += 4
        if r >= 4:
            r -= 4
        c = self.field.check(self.get_coords(r=r))
        if c:
            self.rot = r

    def move(self, d):
        """
        Changes the x-state of the stone by
        d cells; +: right, -: left

        Checks if the move is possible
        for the referenced field and perfoms the move or
        skips the operation.
        """
        c = self.field.check(self.get_coords(x=self.x+d))
        if c:
            self.x += d

    def step(self):
        """
        Changes the y-state of the stone by +1 cell (downwards).

        Internally checks if the move is possible
        for the referenced field and perfoms the move or
        adds the stone coordinates to the field as
        occupied cells.

        Returns True if step was possible, False if stone
        was added to the field.
        """

        c = self.field.check(self.get_coords(y=self.y+1))
        if c:
            self.y += 1
            return True
        else:
            self.field.add(self.get_coords(), self.get_color()) 
            return False
    
    def drop(self):
        """
        Moves the stone down as far down as possible.
        """
        while self.step():
            pass

    def get_coords(self, x=None, y=None, r=None):
        """
        Returns the coordinates of the stone on basis
        of the stone state.
        If states are given as arguments, they are used
        to compute the coordinates, else the object states
        are used.
        """
        x = self.x if x == None else x
        y = self.y if y == None else y
        r = self.rot if r == None else r
        coords = map(lambda coord: (y+coord[0], x+coord[1]), Stone.TYPS[self.typ][r])
        return coords

    def get_color(self):
        """
        Returns the color ID (number) for the stone.
        """
        return Stone.COLS[self.typ]


class Field:
    """
    Represents the field of stones as a w times h array.
    Empty cells have a value of 0, occupied cells have
    a number >0 indicating the color of the cell.
    """
    def __init__(self, h, w):
        self.h = h
        self.w = w
        self.C = [[0 for j in xrange(self.w)] for i in xrange(self.h)]
    
    def check(self, coords):
        """
        Checks that the given coords do not
        exceed the left or right boundary
        and that they do not hit any occupied cell.
        """
        s = True
        for y,x in coords:
            if (x < 0 or x >= self.w or
                y >= self.h or (y >= 0 and self.C[y][x] != 0)):
                s = False
                break
        return s

    def add(self, coords, col):
        """
        Adds the given coordinates
        to the field. The value
        represents the color of
        the occupied cell.
        """
        for y, x in coords:
            self.C[y][x] = col

    def collapse(self):
        """
        Checks if a row is fully
        filled and removes it from
        the field.

        Returns the number of removed rows.
        """
        for y in xrange(self.h-1,-1,-1):
            s = True
            for x in xrange(self.w):
                if self.C[y][x] == 0:
                    s = False
                    break
            if s:
                del self.C[y]

        k = self.h-len(self.C)

        for i in xrange(k):
            self.C.insert(0, [0 for j in xrange(self.w)])                
        return k


def termtris(stdscr, H, W):
    """
    This is the game main loop implemented using the
    curses library.
    """

    ## initialize curses
    stdscr.nodelay(1)
    curses.curs_set(0)
    curses.start_color()
    curses.use_default_colors()
    for i in range(0, curses.COLORS):
        curses.init_pair(i + 1, i, -1)


    _c = 7
    stdscr.addstr(H+3, 2, "____ ____ ____ _    ____ ____ ____ ___", curses.color_pair(_c))
    stdscr.addstr(H+4, 2, "|_ _\\| __\\| . \\|\\/\\ |_ _\\| . \\|___\\| _\\", curses.color_pair(_c))
    stdscr.addstr(H+5, 2, "  || |  ]_|  <_|   \\  || |  <_| /  [__ \\", curses.color_pair(_c))
    stdscr.addstr(H+6, 2, "  |/ |___/|/\_/|/v\/  |/ |/\_/|/   |___/", curses.color_pair(_c))
    stdscr.addstr(H+7, 2, "by senges", curses.color_pair(_c))


    stdscr.addstr(11, W+3, "p - Pause", curses.color_pair(_c))
    stdscr.addstr(12, W+3, "s - Quit game", curses.color_pair(_c))

    stdscr.refresh()

    # initialize game
    f = Field(H, W)    
    win = curses.newwin(H+2, W+2)
    
    points = 0
    rows = 0
    step_per_sec = 0.7

    stone = Stone(0, W/2, random.randint(0,4), f)
    next_stone = Stone(0, W/2, random.randint(0,4), f)

    ## start game loop
    t = time.time()
    while 1:

        ## move stone down by one step
        if time.time()-t > step_per_sec:

            if not stone.step():
                # drop the next stone in the field.
                # check that the new stone does
                # not hit any occupied cells.
                stone = next_stone
                next_stone = Stone(0, W/2, random.randint(0,4), f)

                if not f.check(stone.get_coords()):
                    break   # game is over            

            t = time.time()
            k = f.collapse()

            # compute points and level
            if k > 0:
                rows += k
                points += 2**k * int(10/step_per_sec)
                step_per_sec *= 0.98**k

            stdscr.addstr(1, W+3, "Points %d" % points)
            stdscr.addstr(2, W+3, "Level  %d" % ((rows/6)+1,) )
            stdscr.addstr(3, W+3, "Speed  %0.2f secs/step" % step_per_sec)
            stdscr.addstr(4, W+3, "Next")

            # draw game state on the right on the field
            next_y = 7
            next_x = W+6
            for y in xrange(next_y-1,next_y+3):
                for x in xrange(next_x-1, next_x+2):
                    stdscr.addch(y,x, ord(" "))
            next_coords = next_stone.get_coords(y=next_y, x=next_x)
            for y,x in next_coords:
                stdscr.addch(y, x, curses.ACS_CKBOARD, curses.color_pair(next_stone.get_color()))
            stdscr.refresh()

        win.erase()
        win.border()

        for y in xrange(H):
            for x in xrange(W):
                if f.C[y][x] != 0:
                    win.addch(y+1, x+1, curses.ACS_CKBOARD, curses.color_pair(f.C[y][x]))

        c = stdscr.getch()
        if c == curses.KEY_LEFT:
            stone.move(-1)
        if c == curses.KEY_RIGHT:
            stone.move(1)
        if c == curses.KEY_UP:
            stone.rotate(1)
        if c == curses.KEY_DOWN:
            t -= 1
            continue
        if c == ord("p") or c == ord("P"):
            s = "Pause - Hit any key to continue"
            stdscr.addstr(H+2, 0, " "*40)
            stdscr.addstr(H+2, 0, s)
            stdscr.refresh()
            while 1:
                if stdscr.getch() != -1:
                    break
            stdscr.addstr(H+2, 0, " "*40)
            stdscr.refresh()
        if c == ord("s") or c == ord("S"):
            break
        if c == ord(" "):
            stone.drop()

        coords = stone.get_coords()
        for y,x in coords:
            if y >= 1:
                win.addch(y, x+1, curses.ACS_CKBOARD, curses.color_pair(stone.get_color()))                        

        win.refresh()

    stdscr.addstr(H+2, 0, " "*40)
    s = "Game Over - Ctrl-C to quit"
    stdscr.addstr(H+2, 0, s)
    stdscr.refresh()
    try:
        while 1:
            pass
    except KeyboardInterrupt, e:
        pass

# mainer #####################################

def usage():
    print """Termtris - a terminal tetris game

usage:  termtris OPTIONS

OPTIONS:
  --help:     prints this help message
  -w WIDTH:   width of tetris field
  -h HEIGHT:  height of tetris field
"""
    sys.exit(1)

if __name__ == "__main__":
    import getopt
    W = H = 16
    opts, args =  getopt.getopt(sys.argv[1:], "w:h:", ["help"])
    for k,v in opts:
        if k == "-h":
            H = int(v)
        if k == "-w":
            W = int(v)
        if k == "--help":
            usage()
    try:    
        curses.wrapper(termtris, H, W)
    except KeyboardInterrupt, e:
        pass

